<?php

/**
 * @desc
 *
 **********/
    app::loadLib('phppdo', true);
    class model extends PHPPDO {
        protected $tableName;
        static $dbh;
        private $driverName;
        protected $fields = array();
        
        /**
         * when new the model with parameters, it mean to create a new record, but it won't affect the database
         * if it not use save(). the other hand ,it is a just normal object of the model if no parameters
         * eg:
         *      //create a new record when create a new record
         *      $account = new account(array('username' => 'admin','passwod'=> 'admin'));
         *      $account->save();       
         *
         *      $account = new account();
         *      $account->save();       //throw a exception
         * @param array/null
         */
        function __construct($fields = array()) {
            $dsn = 'mysql://root:root@localhost:3306/site?charset=utf8';
            $className = get_class($this);
            $this->tableName = isset($className::$table) ? $className::$table : $className;
            $this->tableName = config('env.table_prefix') . $this->tableName;
            $opt = $this->_dsn(@parse_url($dsn));
            $newDsn = $opt['scheme'] . ':' . 'host=' . $opt['host'] . ';';
            $newDsn .= 'dbname=' . $opt['path'] . ';';
            $newDsn .= 'port=' . $opt['port'] . ';';
            if(isset($opt['query'])) {
                foreach($opt['query'] as $key => $value) {
                    $newDsn .= $key . '=' . $value . ';';
                }
            }
            $driverOpts = array(
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_OBJ
            );
            parent::__construct($newDsn, $opt['user'], $opt['pass'], $driverOpts);
            if(count($fields)) {
                foreach($fields as $field => $value) {
                    $this->__set($field, $value);
                }
            }

        }
        final function __set($field, $value) {
            $this->fields[$field] = $value;
        }
        final function __get($field) {
            return $this->fields[$field];
        }
        final function __call($fun, $args) {
            $prefix = substr($fun, 0, 4);
            $attr = substr($fun, 4);
            if($prefix == 'set_') {
                $this->__set($attr, $args[0]);
            }
            else if($prefix == 'get_') {
                return $this->__get($attr);
            }
        }
        final function get($fieldName) {
            $fields = $this->fields;
            if(isset($fields[$fieldName])) {
                return $$fields[$fieldName];
            }
            throw new exception('no field ' . $fieldName . ' in table ' . $this->tableName);
        }
        /**
         * query table by primary key field value, it likes findBy()
         * @param mixed primary key field value
         * @return object of a record
         */
        final function findByPk($value) {
            $pkfield = self::pkField();
            return self::findBy($pkfield, $value);
        }
        /**
         * query table by a field value, and return one record as object
         * if you want to get multiple records, use all()
         * eg: yourmodel->findBy('username', 'admin');
         */
        final static function findBy($field, $value) {
            $model = self::_init();
            $query = $model->prepare('SELECT * FROM ' . $model->tableName . ' WHERE ' . $field . ' = ?');
            $query->bindValue(1, $value);
            $query->execute();
            $record = $query->fetch();
            if($record == false) {
                return false;
            }
            return $model->_new($record);
        }
        /**
         * this function is the same as all(), but it return only on record and it's not a static function
         * eg:
         *    example1: yourmodel->find()
         *    example2: yourmodel->find('uid = ? and name = ?', $uid, $username);
         *    example3: yourmodel->find(array('field1','field2','field3'), array('uid = ? and name = ?', $uid, $username));
         *    expmple4: yourmodel->find('id = 1');
         */
        final static function find($where = '') {
            $model = self::_init();
            $args = func_get_args();
            $field = '*';
            $condition = '1';
            $params = array();
            if(count($args) > 0) {
                if(is_array($args[0])) {
                    $field = implode(',', $args[0]);
                    if(isset($args[1]) && !empty($args[1])) {
                        $condition = $args[1][0];
                        $params = array_slice($args[1], 1);
                    }
                }
                else if(is_string($args[0])) {
                    $condition = $args[0];
                    //if(!empty($args[1])) {
                        $params = array_slice($args, 1);
                    //}
                }
            }
            $sql = 'SELECT ' . $field . ' FROM ' . $model->tableName . ' WHERE ' . $condition;
            foreach($params as $param) {
                if(is_array($param)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($param), '?')) . ')',$sql,1);
                }
            }
            $params = arrayChange($params);
            $query = $model->prepare($sql);
            $query->execute($params);
            $record = $query->fetch();
            if($record == false) {
                return false;
            }
            return $model->_new($record);
        }
        /**
         * select all records from table that match with condition
         * eg:
         *    example1: yourmodel::all()
         *    example2: yourmodel::all('uid = ? and name = ?', $uid, $username);
         *    example3: yourmodel::all(array('field1','field2','field3'), array('uid = ? and name = ?', $uid, $username));
         *    expmple4: yourmodel::all('id = 1');
         */
        final static function all($where = '') {
            $model = self::_init();
            $args = func_get_args();
            $field = '*';
            $condition = '1';
            $params = array();
            if(count($args) > 0) {
                if(is_array($args[0])) {
                    $field = implode(',', $args[0]);
                    if(isset($args[1]) && !empty($args[1])) {
                        $condition = $args[1][0];
                        $params = array_slice($args[1], 1);
                    }
                }
                else if(is_string($args[0])) {
                    $condition = $args[0];
                    if(!empty($args[1])) {
                        $params = array_slice($args, 1);
                    }
                }
            }
            $sql = 'SELECT ' . $field . ' FROM ' . $model->tableName . ' WHERE ' . $condition;
            foreach($params as $param) {
                if(is_array($param)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($param), '?')) . ')',$sql,1);
                }
            }
            $params = arrayChange($params);
            $query = $model->prepare($sql);
            $query->execute($params);
            $all = $query->fetchAll();
            $result = array();
            if(count($all) == 0) {
                return array();
            }
            foreach($all as $record) {
                $result[] = $model->_new($record);
            }
            return $result;
        }
        /**
         * this function have two means, update or create. when record's field value changed,save means update;
         * and if create this model object with parameters, save means create a new record
         */
        final function save() {
            if(count($this->fields)) {
                $pk = $this->pkField();
                $keys = array_keys($this->fields);
                $values = array_values($this->fields);
                $method = 'insert';
                if(isset($this->fields[$pk]) && $this->findByPk($this->fields[$pk])) {
                    $method = 'update';
                    $sql = 'UPDATE ' . $this->tableName . 'SET (' . implode(',', $keys) . ')';
                    $sql .= ' VALUES (' . implode(',', array_fill(0, count($keys), '?')) . ')';
                }
                else {
                    $sql = 'INSERT INTO ' . $this->tableName . ' (' . implode(',', $keys) . ')';
                    $sql .= ' VALUES (' . implode(',', array_fill(0, count($keys), '?')) . ')';
                }
            }
            else {
                throw new exception('no value to update or create as a new record');
            }
            $query = $this->prepare($sql);
            $result = $query->execute($values);
            if($result) {
                $insertId = $this->lastInsertId();
                $fields = $this->fields();
                $autoCreateField = 'id';
                foreach($fields as $field) {
                    if($field->Extra == 'auto_increment') {
                        $autoCreateField = $field->Field;
                        break;
                    }
                }
                $latest = $this->findBy($autoCreateField, $insertId);
                $this->fields = array_merge($this->fields, $latest->fields);
            }
        }
        /**
         * create a record use this static method, it will affect the database, and return the object-record of the model
         * @param array
         * @return object
         */
        final static function create($opts) {
            if(!is_array($opts)) {
                throw new exception('field(s) and value(s) must contruct as an array');
            }
            $className = get_called_class();
            $model = new $className($opts);
            $model->save();
            return $model;
        }
        /**
         * update a record's field values directly
         * @param array
         */
        final function update($opts) {
            if(is_array($opts)) {
                $pk = $this->pkField();
                $keys = array_keys($opts);
                $values = array_values($opts);
                if(isset($this->fields[$pk]) && $this->findByPk($this->fields[$pk])) {
                    $sql = 'UPDATE ' . $this->tableName . ' SET ' . implode(' = ?,', $keys) . ' = ?';
                    $sql .= ' WHERE ' . $pk . '= ?';
                    $query = $this->prepare($sql);
                    array_push($values, $this->fields[$pk]);
                    $query->execute($values);
                }
                else {
                    throw new exception('no record to update');
                }
            }
            else {
                throw new exception('param error, it must be an array');
            }
        }
        /**
         * update table records, and retuen the effect rows
         * eg:
         *      example1: yourmodel::updateAll(array('userType' => 1, 'userLoation' => 'china'));
         *      example2: yourmodel::updateAll(array('userType' => 1, 'userLoation' => 'china'), array('id in(?)', array(1,2,3)));
         *      example3: yourmodel::updateAll(array('userType' => 1, 'userLoation' => 'china'), 'id != 1');
         * @param (array) new values to update
         * @param (mixed) filter the update effect records
         * @return effect row number
         */
        final static function updateAll($opts, $where = '') {
            if(!is_array($opts)) {
                throw new exception('param error, it must be an array');
            }
            $model = self::_init();
            $keys = array_keys($opts);
            $values = array_values($opts);
            $condition = '1';
            if(is_array($where)) {
                $condition = $where[0];
                $values = array_merge($values, array_slice($where, 1));
            }
            else if(is_string($where) && $where != '') {
                 $condition = $where;
            }
            $sql = 'UPDATE ' . $model->tableName . ' SET ' . implode(' = ?,', $keys) . ' = ?';
            $sql .= ' WHERE ' . $condition;
            foreach($values as $value) {
                if(is_array($value)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($value), '?')) . ')', $sql, 1);
                }
            }
            $values = arrayChange($values);
            $query = $model->prepare($sql);
            $query->execute($values);
            return $query->rowCount();
        }
        /**
         * delete current record from the table
         */
        final function delete() {
            $pk = $this->pkField();
            if(isset($this->fields[$pk]) && $this->findByPk($this->fields[$pk])) {
                $sql = 'DELETE FROM ' . $this->tableName . ' WHERE ' . $pk . ' = ?';
                $query = $this->prepare($sql);
                $query->execute(array($this->fields[$pk]));
                //return $query->rowCount();
            }
            else {
                throw new exception('no record to delete');
            }
        }
        /**
         * delete all records from table which are matched with filter
         * eg:
         *    1. yourmodel::deleteAll();
         *    2. yourmodel::deleteAll('id = 1');
         *    3. yourmodel::deleteAll('id in(?)', array(1,2,3));
         * @param (mixed) condition for delete
         * @return (int) effect rows
         */
        final static function deleteAll($where = '') {
            $model = self::_init();
            $args = func_get_args();
            $condition = '1';
            if(!is_string($args[0])) {
                throw new exception('param error, statement must be a string');
            }
            if(!empty($args[0])) {
                $condition = $args[0];
            }
            $sql = 'DELETE FROM ' . $model->tableName . ' WHERE ' . $condition;
            $values = array_slice($args, 1);
            foreach($values as $value) {
                if(is_array($value)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($value), '?')) . ')', $sql, 1);
                }
            }
            $values = arrayChange($values);
            $query = $model->prepare($sql);
            $query->execute($values);
            return $query->rowCount();
        }
        final static function pagination($where = '') {
            $model = self::_init();
            $args = func_get_args();
            $fields = '*';
            if(is_array($args[0])) {
                $fields = implode(',', $args[0]);
                $start = $args[1];
                $limit = $args[2];
            }
            else {
                $start = $args[0];
                $limit = $args[1];
            }
            $condition = '1';
            $values = array();
            $where = end($args);
            if(is_string($where) && !empty($where)) {
                $condition = $where;
            }
            else if(is_array($where)) {
                $condition = $where[0];
                $values = array_slice($where, 1);
            }
            $sql = 'SELECT ' . $fields . ' FROM ' . $model->tableName . ' WHERE ' . $condition . ' LIMIT ?,?';
            array_push($values, $start, $limit);
            foreach($values as $value) {
                if(is_array($value)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($value), '?')) . ')', $sql, 1);
                }
            }
            $values = arrayChange($values);
            $query = $model->prepare($sql);
            $query->execute($values);
            $records = $query->fetchAll();
            $result = array();
            foreach($records as $record) {
                $result[] = $model->_new($record);
            }
            return $result;
        }
        /**
         * count records that match wit filter
         * eg: 
         *    1. yourmodel::count();
         *    2. yourmodel::count('username', array('id = 1'));
         *    3. yourmodel::count('username', arrat('id > ? and type = ?', 3, 'system'));
         * @param (mixed)
         * @return (int) count number
         */
        final static function count($where = '') {
            $model = self::_init();
            $args = func_get_args();
            $condition = '1';
            $params = array();
            $field = count($args) == 2 ? $args[0] : '*';
            $where = count($args) == 2 ? $args[1] : $args;
            if(!empty($where)) {
                $condition = $where[0];
            }
            $params = array_slice($where, 1);
            $sql = 'SELECT COUNT(' . $field . ') FROM ' . $model->tableName . ' WHERE ' . $condition;
            foreach($params as $param) {
                if(is_array($param)) {
                   $sql = preg_replace('/\(\?\)/', '(' . implode(',', array_fill(0, count($param), '?')) . ')', $sql, 1);
                }
            }
            $params = arrayChange($params);
            $query = $model->prepare($sql);
            $query->execute($params);
            $count = $query->fetchColumn();
            return $count;
        }
        final static function tables() {
            $model = self::_init();
            $query = $model->query('SHOW TABLES');
            $tables = $query->fetchAll(PDO::FETCH_NUM);
            return arrayChange($tables);
        }
        final static  function databases() {
            $model = self::_init();
            $query = $model->query('SHOW DATABASES');
            $dbs = $query->fetchAll(PDO::FETCH_NUM);
            return arrayChange($dbs);
        }
        final static  function pkField() {
            $model = self::_init();
            $fields = $model->fields();
            foreach($fields as $field) {
                if($field->Key === "PRI") {
                    return $field->Field;
                }
            }
            throw new exception('no primary field set in table ' . $model->tableName);
        }
        final static function fields() {
            $model = self::_init();
            $query = $model->prepare('SHOW COLUMNS FROM ' . $model->tableName);
            $query->execute();
            $fields = $query->fetchAll(PDO::FETCH_OBJ);
            return $fields;
        }

        final static function _init() {
            $className = get_called_class();
            if(self::$dbh === null) {
                self::$dbh = new $className();
            }
            return self::$dbh;
        }
        final private function _dsn($opts) {
            $opts['path'] = trim($opts['path'], '/');
            if(empty($opts['query'])) {
                return $opts;
            }
            $str = explode('&', $opts['query']);
            $params = array();
            foreach($str as $item) {
                $tmp = explode('=', $item);
                $params[$tmp[0]] = $tmp[1];
            }
            $opts['query'] = $params;
            return $opts;
        }
        final private function _new($record) {
            $className = get_called_class();
            $class = new $className();
            foreach($record as $key => $value) {
                $method = 'set_' . $key;
                $class->$method($value);
            }
            return $class;
        }
    }
    class testModel extends model{
        static $table = 'admin';
    }
?>